package dada

import (
	"bytes"
	"context"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"
)

const (
	defaultBaseURL = "https://newopen.imdada.cn/"
	testBaseURL    = "https://newopen.qa.imdada.cn/"
	userAgent      = "go-dada"
)

// Config 达达配置
type Config struct {
	AppKey    string `mapstructure:"app_key"`
	AppSecret string `mapstructure:"app_secret"`
	Callback  string `mapstructure:"callback"`
	Debug     bool   `mapstructure:"debug"`
}

// Options 公共请求参数
type Options struct {
	Debug bool // true 测试环境
}

// Option 参数配置方法
type Option func(*Options)

// Debug 是否启用测试环境
func Debug(debug bool) Option {
	return func(o *Options) {
		o.Debug = debug
	}
}

// Client manages communication with the dada API.
type Client struct {
	client *http.Client
	o      *Options

	AppKey    string
	AppSecret string
	// Base URL for API.requests.
	// BaseURL should always be specified with a trailing slash.
	BaseURL *url.URL

	// User agent used when communication with dada API.
	UserAgent string

	common service // Reuse a single struct instead of allocating one for each service on the heap.

	// Services used for talking to different parts of dada API.
	Order *OrderService
}

type service struct {
	client *Client
}

// NewClient returns a new dada API client.
func NewClient(appKey string, appSecret string, setters ...Option) *Client {
	options := &Options{}
	for _, setter := range setters {
		setter(options)
	}
	_url := defaultBaseURL
	if options.Debug {
		_url = testBaseURL
	}
	baseURL, _ := url.Parse(_url)

	c := Client{BaseURL: baseURL, UserAgent: userAgent, client: &http.Client{}, AppKey: appKey, AppSecret: appSecret}
	c.common.client = &c
	c.Order = (*OrderService)(&c.common)
	return &c
}

// NewRequest creates an API request. A relative URL can be provided in urlStr,
// in which case it is resolved relative to the BaseURL of the Client.
// Relative URLs should always be specified without a preceding slash.
// If specified, the value pointed to by body is JSON encoded and included as
// request body.
func (c *Client) NewRequest(urlStr string, sourceID string, body interface{}) (*http.Request, error) {
	u, err := c.BaseURL.Parse(urlStr)
	if err != nil {
		return nil, err
	}
	parameters := map[string]string{
		"app_key":   c.AppKey,
		"body":      "",
		"format":    "json",
		"source_id": sourceID,
		"timestamp": strconv.Itoa(int(time.Now().Unix())),
		"v":         "1.0",
	}

	var rawBody string
	var buf bytes.Buffer
	if body != nil {
		enc := json.NewEncoder(&buf)
		enc.SetEscapeHTML(false)
		if err := enc.Encode(body); err != nil {
			return nil, err
		}
		data, _ := json.Marshal(body)
		rawBody = string(data)
	}
	parameters["body"] = rawBody
	var signature string
	if signature, err = c.Sign(parameters); err != nil {
		return nil, err
	}
	parameters["signature"] = signature
	data, _ := json.Marshal(parameters)
	req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBuffer(data))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", "application/json")

	if c.UserAgent != "" {
		req.Header.Set("User-Agent", c.UserAgent)
	}
	return req, nil
}

// Sign 参数签名
func (c *Client) Sign(values map[string]string) (string, error) {
	var buf strings.Builder
	keys := make([]string, 0, len(values))
	for k := range values {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	buf.WriteString(c.AppSecret)
	for _, k := range keys {
		v := values[k]
		buf.WriteString(k)
		buf.WriteString(v)
	}
	buf.WriteString(c.AppSecret)
	hash := md5.Sum([]byte(buf.String()))
	return strings.ToUpper(hex.EncodeToString(hash[:])), nil

}

// Response represents dada Response.
type Response struct {
	*http.Response

	Status  string          `json:"status"`
	Code    int             `json:"code"`
	ErrCode int             `json:"errCode"`
	Msg     string          `json:"msg"`
	Result  json.RawMessage `json:"result"`
}

// ErrorResponse represents dada Error Response.
type ErrorResponse struct {
	*http.Response
	requestBody []byte

	Status    string `json:"status"`
	Code      int    `json:"code"`
	ErrorCode int    `json:"errorCode"`
	Msg       string `json:"msg"`
}

// Error implements builtin.error interface.
func (r *ErrorResponse) Error() string {
	return fmt.Sprintf("%v %v body %s : %d %v",
		r.Response.Request.Method, r.Response.Request.URL, r.requestBody,
		r.Code, r.Msg)
}

func newResponse(r *http.Response) (*Response, error) {
	response := Response{Response: r}
	data, err := ioutil.ReadAll(r.Body)
	if err == nil && data != nil {
		// ignore err because maybe image response
		_ = json.Unmarshal(data, &response)
	}
	r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
	return &response, nil
}

// CheckResponse checks the API response for errors, and returns them if present.
// API error responses are expected to have response body.
// If none error find, return dada.Response.
func CheckResponse(r *Response, reqBody []byte) error {
	errResponse := &ErrorResponse{
		Response:    r.Response,
		requestBody: reqBody,
		Status:      r.Status,
		Code:        r.Code,
		ErrorCode:   r.ErrCode,
		Msg:         r.Msg,
	}
	switch errResponse.Code {
	case 0:
		return nil
	default:
		return errResponse
	}
}

// Do Sends an API request and returns the API response. The API response is
// JSON decoded and stored in the value pointed to by v, or returned as an
// error if an API error has occurred. If v implements the io.Writer interface,
// the raw response body will be written to v, without attempting to first
// decode it.
//
// The provided ctx must be non-nil, if it is nil an error is returned. If it
// is canceled or timeout, ctx.Err() will be returned.
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) {
	if ctx == nil {
		return nil, errors.New("context must be non-nil")
	}

	req = req.WithContext(ctx)

	// save body for display request error info.
	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		return nil, errors.New("read request body error")
	}

	req.Body = ioutil.NopCloser(bytes.NewBuffer(body))

	resp, err := c.client.Do(req)
	if err != nil {
		// If we got an error, and the context has been canceled,
		// the context's error id probably more useful.
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		default:
		}
		return nil, err

	}

	response, err := newResponse(resp)
	if err != nil {
		return nil, err
	}

	if err := CheckResponse(response, body); err != nil {
		return resp, err
	}

	if v != nil {
		var data io.Reader
		data = bytes.NewReader(response.Result)
		decErr := json.NewDecoder(data).Decode(v)
		if decErr == io.EOF {
			decErr = nil // ignore io.EOF errors caused by empty response body
		}
		if decErr != nil {
			err = decErr
		}
	}
	return resp, err

}

// Signature 签名校验
func Signature(clientID, outTradeNo, updateTime string) string {
	ss := []string{clientID, outTradeNo, updateTime}
	sort.Strings(ss)
	var b bytes.Buffer
	for _, s := range ss {
		b.WriteString(s)
	}
	hash := md5.Sum(b.Bytes())
	return hex.EncodeToString(hash[:])
}
